module Msf::Exploit::Remote::Kerberos::Ticket::Storage
  class Base
    extend Forwardable
    include Msf::Auxiliary::Report

    # @!attribute [r] framework
    #   @return [Msf::Framework] the Metasploit framework instance
    attr_reader :framework

    # @!attribute [r] framework_module
    #   @return [Msf::Module] the Metasploit framework module that is associated with the authentication instance
    attr_reader :framework_module

    def initialize(framework: nil, framework_module: nil)
      @framework = framework || framework_module&.framework
      @framework_module = framework_module
    end

    # Delete tickets matching the options query.
    #
    # @param [Hash] options See the options hash description in {#tickets}.
    # @option options [Array<Integer>] :ids The identifiers of the tickets to delete (optional)
    # @return [Array<StoredTicket>]
    def delete_tickets(options = {})
      []
    end

    # Get stored tickets matching the options query.
    #
    # @param [Hash] options The options for matching tickets. The :realm, :server, :client and :status options are all
    #   processed as a group. If any one or more of them are specified, they are all used for filtering. It can not for
    #   example specify client and fetch all tickets for a particular client where the server is different.
    # @option options [Integer, Array<Integer>] :id The identifier of the ticket (optional)
    # @option options [String] :host The host for the ticket (optional)
    # @option options [String] :realm The realm of the ticket (optional)
    # @option options [String] :server The service name of the ticket (optional)
    # @option options [String] :client The client username of the ticket (optional)
    # @option options [Symbol] :status The ticket status, defaults to valid (optional)
    # @return [Array<StoredTicket>]
    def tickets(options = {}, &block)
      []
    end

    # Load a stored credential object that is suitable for authentication.
    #
    # @param [Hash] options See the options description in #tickets.
    # @return [Rex::Proto::Kerberos::CredentialCache::Krb5CcacheCredential, nil] credential The credential if one was
    #   found.
    def load_credential(options = {})
      nil
    end

    # Store the specified object.
    #
    # @param [Rex::Proto::Kerberos::CredentialCache::Krb5Ccache] ccache The credential cache object to store.
    # @param [Hash] options The information associated with the stored object. See the options hash description in
    #   {#tickets}.
    # @return [Hash]
    #   * :path [String] The path to the persisted ccache file if successful
    def store_ccache(ccache, options = {})
      {}
    end

    # Mark ccache(s) as inactive
    #
    # @param [Array<Integer>] ids The list of ccache IDs.
    # @return [Array<StoredTicket>]
    def deactivate_ccache(ids)
      []
    end

    # Mark ccache(s) as active
    #
    # @param [Array<Integer>] ids The list of ccache IDs.
    # @return [Array<StoredTicket>]
    def activate_ccache(ids)
      []
    end

    # @return [String] The name of the workspace in which to operate.
    def workspace
      if @framework_module
        return @framework_module.workspace
      elsif @framework&.db&.active
        return @framework.db.workspace&.name
      end
    end

    private

    # Forward whatever method calls this to the specified framework module, but
    # only if it's present otherwise return nil.
    def proxied_module_method(*args, **kwargs)
      return nil unless @framework_module

      @framework_module.send(__callee__, *args, **kwargs)
    end

    alias :print_good :proxied_module_method
    alias :print_status :proxied_module_method

    # Return the raw stored objects.
    #
    # @param [Hash] options See the options hash description in {#tickets}.
    # @return [Array<Mdm::Loot>]
    def objects(options, &block)
      return [] unless active_db?

      filter = {}
      filter[:id] = options[:id] if options[:id].present?
      if options[:host].present?
        if options[:host].is_a?(Mdm::Host)
          filter[:host] = options[:host]
        else
          filter[:host] = { address: options[:host] }
        end
      end
      unless (info = loot_info(options)).blank?
        filter[:info] = JSON.generate(info)
      end
      loots = framework.db.loots(workspace: options.fetch(:workspace) { workspace }, ltype: 'mit.kerberos.ccache', **filter).select do |loot|
        # Remove any loot that isn't 'mit.kerberos.ccache'
        # Necessary when an `id` is provided in which case the `ltype` search above is ignored
        loot.ltype == 'mit.kerberos.ccache'
        # Normally if you search for an id that does not exist in loots an `ActiveRecord::RecordNotFound` exception is raised
        # Since the record does exist no exception is raised but that makes the user experience inconsistent
        # I would feel weird about throwing an exception at this point so I decided not to and just deal with the inconsistency
      end
      loots.each do |stored_loot|
        block.call(stored_loot) if block_given?
      end
    end

    # Build a loot info string that can later be used in a lookup.
    #
    # @return [Hash] the info hash
    def loot_info(options = {})
      info = {}

      status = options.fetch(:status, :valid).downcase.to_sym
      info[:status] = status unless status == :valid

      realm = options[:realm]
      info[:realm] = realm.to_s.upcase if realm.present?

      client = options[:client]
      info[:client] = client.to_s.downcase if client.present?

      server = options[:server]
      info[:server] = server.to_s.downcase if server.present?

      info
    end
  end
end
